package com.chengqs.summer.io;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/**
 * 资源解析器 (ResourceResolver)
 *
 * 这个类用于扫描和解析某个指定包路径下的资源文件。它主要用于在给定的包路径中查找资源，并将其转换为一个可以处理的对象列表。
 * 该类在实现中使用了一些设计模式，使得它具有高度的灵活性和可扩展性。
 *
 * 该类使用的设计模式：
 *
 * <h2>1. 策略模式（Strategy Pattern）(行为):</h2>
 * <p>
 *    - `scan` 方法接受一个 `Function<Resource, R> mapper` 参数，允许用户传入不同的策略对资源进行处理。
 *    - 这样可以将资源处理逻辑与扫描逻辑分离，用户可以定义自己的处理方式。
 * </p>
 *
 * <h2>2. 模板方法模式（Template Method Pattern）(行为):</h2>
 * <p>
 *    - `scan` 和 `scan0` 方法定义了资源扫描的基本流程和细节实现。
 *    - 扫描过程的框架由 `scan` 方法提供，而具体的扫描细节则由 `scan0` 实现。
 * </p>
 *
 * <h2>3. 工厂模式（Factory Pattern）(创建):</h2>
 * <p>
 *    - `scanFile` 方法中根据不同类型创建 `Resource` 对象。
 *    - 工厂模式允许 `Resource` 对象的灵活创建，使得扩展不同的资源类型变得容易。
 * </p>
 *
 * <h2>4. 迭代器模式（Iterator Pattern）(行为):</h2>
 * <p>
 *    - 资源和文件的遍历使用了迭代器模式，抽象了遍历的细节。
 *    - 例如，`scan0` 方法中通过 `Enumeration<URL>` 和 `Files.walk()` 实现资源的遍历。
 * </p>
 */
public class ResourceResolver {

    Logger logger = LoggerFactory.getLogger(getClass());

    String basePackage; // 保存需要解析的基础包路径

    /**
     * 构造函数 -- 接受一个字符串参数 basePackage，用于初始化基础包路径。这个路径会用于扫描资源时进行路径解析。
     * @param basePackage
     */
    public ResourceResolver(String basePackage) {
        this.basePackage = basePackage;
    }

    /**
     * 这个方法用于扫描基础包路径下的所有资源，并将这些资源应用给定的函数 mapper
     * @param mapper 用于对每个找到的资源进行处理和转换
     * @return 映射后的结果列表。
     * @param <R> 映射后的结果类型。
     */
    public <R> List<R> scan(Function<Resource, R> mapper) {
        // 将 basePackage 转换为文件系统中的路径格式（将.替换为/）
        String basePackagePath = this.basePackage.replace(".", "/");
        String path = basePackagePath;
        try {
            // 创建一个空的 List<R> 作为收集器 collector。
            List<R> collector = new ArrayList<>();
            // 实际执行扫描的私有方法，传入基础包路径、转换后的路径、结果收集器和映射函数。
            scan0(basePackagePath, path, collector, mapper);
            return collector;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     *  这个方法是 scan 方法的辅助方法，负责具体的资源查找过程。
     * @param basePackagePath 基础包路径。
     * @param path 转换后的路径。
     * @param collector 用于收集扫描结果的列表。
     * @param mapper 映射函数。
     * @param <R> 映射后的结果类型。
     * @throws IOException 如果发生 I/O 错误。
     * @throws URISyntaxException 如果 URI 格式不正确。
     */
    <R> void scan0(String basePackagePath, String path, List<R> collector, Function<Resource, R> mapper) throws IOException, URISyntaxException {
        logger.atDebug().log("scan path: {}", path);
        // 获取资源：使用 getContextClassLoader().getResources(path) 来获取所有匹配路径的资源 URL 列表。
        Enumeration<URL> en = getContextClassLoader().getResources(path);
        // 遍历资源：对每个 URL：
        while (en.hasMoreElements()) {
            URL url = en.nextElement();
            // 转换为 URI。
            URI uri = url.toURI();
            // 去掉尾部斜杠并将 URI 转换为字符串。
            String uriStr = removeTrailingSlash(uriToString(uri));
            // 通过 uriStr 和 basePackagePath 计算出 uriBaseStr。
            String uriBaseStr = uriStr.substring(0, uriStr.length() - basePackagePath.length());
            // 根据 uriBaseStr 前缀判断是文件系统还是 JAR 文件。
            if (uriBaseStr.startsWith("file:")) {
                uriBaseStr = uriBaseStr.substring(5);
            }
            if (uriStr.startsWith("jar:")) {
                // 调用 scanFile 方法继续扫描文件。
                scanFile(true, uriBaseStr, jarUriToPath(basePackagePath, uri), collector, mapper);
            } else {
                // 调用 scanFile 方法继续扫描文件。
                scanFile(false, uriBaseStr, Paths.get(uri), collector, mapper);
            }
        }
    }

    /**
     * 获取当前线程的上下文类加载器。如果线程的上下文类加载器为空，则使用当前类的类加载器。
     * @return 返回一个 ClassLoader 对象，用于加载资源。
     */
    ClassLoader getContextClassLoader() {
        ClassLoader cl = null;
        cl = Thread.currentThread().getContextClassLoader();
        if (cl == null) {
            cl = getClass().getClassLoader();
        }
        return cl;
    }

    /**
     * 将 JAR 文件中的 URI 转换为文件系统路径。
     * @param basePackagePath 基础包路径。
     * @param jarUri JAR 文件的 URI。
     * @return 返回一个 Path 对象，表示 JAR 文件内的路径。
     * @throws IOException 如果发生 I/O 错误。
     */
    Path jarUriToPath(String basePackagePath, URI jarUri) throws IOException {
        return FileSystems.newFileSystem(jarUri, Map.of()).getPath(basePackagePath);
    }

    /**
     * 在指定的路径下递归扫描文件，并将每个文件包装成 Resource 对象，然后应用映射函数。
     * @param isJar 表示是否在 JAR 文件中扫描。
     * @param base 基础路径。
     * @param root 要扫描的起始路径。
     * @param collector 用于收集扫描结果的列表。
     * @param mapper 映射函数。
     * @param <R> 映射后的结果类型。
     * @throws IOException 如果发生 I/O 错误。
     */
    <R> void scanFile(boolean isJar, String base, Path root, List<R> collector, Function<Resource, R> mapper) throws IOException {
        // 去掉 base 路径中的尾部斜杠。
        String baseDir = removeTrailingSlash(base);
        // 遍历文件：使用 Files.walk(root) 遍历目录下的所有文件。
        Files.walk(root).filter(Files::isRegularFile).forEach(file -> {
            Resource res = null;
            // 判断是 JAR 还是文件系统：
            if (isJar) {
                // 如果是 JAR，创建一个 Resource 对象，并使用 removeLeadingSlash 方法去掉路径前的斜杠。
                res = new Resource(baseDir, removeLeadingSlash(file.toString()));
            } else {
                // 如果是文件系统，计算出文件的相对路径，并创建 Resource 对象。
                String path = file.toString();
                String name = removeLeadingSlash(path.substring(baseDir.length()));
                res = new Resource("file:" + path, name);
            }
            logger.atDebug().log("found resource: {}", res);
            // 将资源应用映射函数
            R r = mapper.apply(res);
            if (r != null) {
                // 将结果添加到 collector
                collector.add(r);
            }
        });
    }

    /**
     * 将 URI 转换为字符串，并使用 UTF-8 进行解码。
     * @param uri URI uri 是要转换的 URI。
     * @return 返回解码后的字符串。
     */
    String uriToString(URI uri) {
        return URLDecoder.decode(uri.toString(), StandardCharsets.UTF_8);
    }

    /**
     * 去掉字符串开头的斜杠。
     * @param s 要处理的字符串。
     * @return 返回处理后的字符串。
     */
    String removeLeadingSlash(String s) {
        if (s.startsWith("/") || s.startsWith("\\")) {
            s = s.substring(1);
        }
        return s;
    }

    /**
     * 去掉字符串结尾的斜杠。
     * @param s 要处理的字符串。
     * @return 返回处理后的字符串。
     */
    String removeTrailingSlash(String s) {
        if (s.endsWith("/") || s.endsWith("\\")) {
            s = s.substring(0, s.length() - 1);
        }
        return s;
    }
}
